/**
 * L2FProd.com Common Components 6.9.1 License.
 *
 * Copyright 2005-2011 L2FProd.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.l2fprod.common.swing;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;

/**
 * PercentLayout. <BR>Constraint based layout which allow the space to be
 * splitted using percentages. The following are allowed when adding components
 * to container:
 * <ul>
 * <li>container.add(component); <br>in this case, the component will be
 * sized to its preferred size
 * <li>container.add(component, "100"); <br>in this case, the component will
 * have a width (or height) of 100
 * <li>container.add(component, "25%"); <br>in this case, the component will
 * have a width (or height) of 25 % of the container width (or height) <br>
 * <li>container.add(component, "*"); <br>in this case, the component will
 * take the remaining space. if several components use the "*" constraint the
 * space will be divided among the components.
 * </ul>
 * 
 * @javabean.class
 *          name="PercentLayout"
 *          shortDescription="A layout supports constraints expressed in percent."
 */
public class PercentLayout implements LayoutManager2 {

    /**
     * Useful constant to layout the components horizontally (from top to
     * bottom).
     */
    public final static int HORIZONTAL = 0;

    /**
     * Useful constant to layout the components vertically (from left to right).
     */
    public final static int VERTICAL = 1;

    static class Constraint {
        protected Object value;

        private Constraint(Object value) {
            this.value = value;
        }
    }

    static class NumberConstraint extends Constraint {
        public NumberConstraint(int d) {
            this(new Integer(d));
        }

        public NumberConstraint(Integer d) {
            super(d);
        }

        public int intValue() {
            return ((Integer) value).intValue();
        }
    }

    static class PercentConstraint extends Constraint {
        public PercentConstraint(float d) {
            super(new Float(d));
        }

        public float floatValue() {
            return ((Float) value).floatValue();
        }
    }

    private final static Constraint REMAINING_SPACE = new Constraint("*");

    private final static Constraint PREFERRED_SIZE = new Constraint("");

    private int orientation;
    private int gap;

    private Hashtable m_ComponentToConstraint;

    /**
     * Creates a new HORIZONTAL PercentLayout with a gap of 0.
     */
    public PercentLayout() {
        this(HORIZONTAL, 0);
    }

    public PercentLayout(int orientation, int gap) {
        setOrientation(orientation);
        this.gap = gap;

        m_ComponentToConstraint = new Hashtable();
    }

    public void setGap(int gap) {
        this.gap = gap;
    }

    /**
     * @javabean.property
     *          bound="true"
     *          preferred="true"
     */
    public int getGap() {
        return gap;
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL && orientation != VERTICAL) {
            throw new IllegalArgumentException("Orientation must be one of HORIZONTAL or VERTICAL");
        }
        this.orientation = orientation;
    }

    /**
     * @javabean.property
     *          bound="true"
     *          preferred="true"
     */
    public int getOrientation() {
        return orientation;
    }

    public Constraint getConstraint(Component component) {
        return (Constraint) m_ComponentToConstraint.get(component);
    }

    public void setConstraint(Component component, Object constraints) {
        if (constraints instanceof Constraint) {
            m_ComponentToConstraint.put(component, constraints);
        } else if (constraints instanceof Number) {
            setConstraint(component, new NumberConstraint(((Number) constraints).intValue()));
        } else if ("*".equals(constraints)) {
            setConstraint(component, REMAINING_SPACE);
        } else if ("".equals(constraints)) {
            setConstraint(component, PREFERRED_SIZE);
        } else if (constraints instanceof String) {
            String s = (String) constraints;
            if (s.endsWith("%")) {
                float value = Float.valueOf(s.substring(0, s.length() - 1)).floatValue() / 100;
                if (value > 1 || value < 0)
                    throw new IllegalArgumentException("percent value must be >= 0 and <= 100");
                setConstraint(component, new PercentConstraint(value));
            } else {
                setConstraint(component, new NumberConstraint(Integer.valueOf(s)));
            }
        } else if (constraints == null) {
            // null constraint means preferred size
            setConstraint(component, PREFERRED_SIZE);
        } else {
            throw new IllegalArgumentException("Invalid Constraint");
        }
    }

    public void addLayoutComponent(Component component, Object constraints) {
        setConstraint(component, constraints);
    }

    /**
     * Returns the alignment along the x axis. This specifies how the component
     * would like to be aligned relative to other components. The value should be
     * a number between 0 and 1 where 0 represents alignment along the origin, 1
     * is aligned the furthest away from the origin, 0.5 is centered, etc.
     */
    public float getLayoutAlignmentX(Container target) {
        return 1.0f / 2.0f;
    }

    /**
     * Returns the alignment along the y axis. This specifies how the component
     * would like to be aligned relative to other components. The value should be
     * a number between 0 and 1 where 0 represents alignment along the origin, 1
     * is aligned the furthest away from the origin, 0.5 is centered, etc.
     */
    public float getLayoutAlignmentY(Container target) {
        return 1.0f / 2.0f;
    }

    /**
     * Invalidates the layout, indicating that if the layout manager has cached
     * information it should be discarded.
     */
    public void invalidateLayout(Container target) {
    }

    /**
     * Adds the specified component with the specified name to the layout.
     * 
     * @param name the component name
     * @param comp the component to be added
     */
    public void addLayoutComponent(String name, Component comp) {
    }

    /**
     * Removes the specified component from the layout.
     * 
     * @param comp the component ot be removed
     */
    public void removeLayoutComponent(Component comp) {
        m_ComponentToConstraint.remove(comp);
    }

    /**
     * Calculates the minimum size dimensions for the specified panel given the
     * components in the specified parent container.
     * 
     * @param parent the component to be laid out
     * @see #preferredLayoutSize
     */
    public Dimension minimumLayoutSize(Container parent) {
        return preferredLayoutSize(parent);
    }

    /**
     * Returns the maximum size of this component.
     * 
     * @see java.awt.Component#getMinimumSize()
     * @see java.awt.Component#getPreferredSize()
     * @see java.awt.LayoutManager
     */
    public Dimension maximumLayoutSize(Container parent) {
        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
    }

    public Dimension preferredLayoutSize(Container parent) {
        Component[] components = parent.getComponents();
        Insets insets = parent.getInsets();
        int width = 0;
        int height = 0;
        Dimension componentPreferredSize;
        boolean firstVisibleComponent = true;
        for (int i = 0, c = components.length; i < c; i++) {
            if (components[i].isVisible()) {
                componentPreferredSize = components[i].getPreferredSize();
                if (orientation == HORIZONTAL) {
                    height = Math.max(height, componentPreferredSize.height);
                    width += componentPreferredSize.width;
                    if (firstVisibleComponent) {
                        firstVisibleComponent = false;
                    } else {
                        width += gap;
                    }
                } else {
                    height += componentPreferredSize.height;
                    width = Math.max(width, componentPreferredSize.width);
                    if (firstVisibleComponent) {
                        firstVisibleComponent = false;
                    } else {
                        height += gap;
                    }
                }
            }
        }
        return new Dimension(width + insets.right + insets.left, height + insets.top + insets.bottom);
    }

    public void layoutContainer(Container parent) {
        Insets insets = parent.getInsets();
        Dimension d = parent.getSize();

        // calculate the available sizes
        d.width = d.width - insets.left - insets.right;
        d.height = d.height - insets.top - insets.bottom;

        // pre-calculate the size of each components
        Component[] components = parent.getComponents();
        int[] sizes = new int[components.length];

        // calculate the available size
        int totalSize = (HORIZONTAL == orientation ? d.width : d.height) - (components.length - 1) * gap;
        int availableSize = totalSize;

        // PENDING(fred): the following code iterates 4 times on the component
        // array, need to find something more efficient!

        // give priority to components who want to use their preferred size or who
        // have a predefined size
        for (int i = 0, c = components.length; i < c; i++) {
            if (components[i].isVisible()) {
                Constraint constraint = (Constraint) m_ComponentToConstraint.get(components[i]);
                if (constraint == null || constraint == PREFERRED_SIZE) {
                    sizes[i] = (HORIZONTAL == orientation ? components[i].getPreferredSize().width : components[i].getPreferredSize().height);
                    availableSize -= sizes[i];
                } else if (constraint instanceof NumberConstraint) {
                    sizes[i] = ((NumberConstraint) constraint).intValue();
                    availableSize -= sizes[i];
                }
            }
        }

        // then work with the components who want a percentage of the remaining
        // space
        int remainingSize = availableSize;
        for (int i = 0, c = components.length; i < c; i++) {
            if (components[i].isVisible()) {
                Constraint constraint = (Constraint) m_ComponentToConstraint.get(components[i]);
                if (constraint instanceof PercentConstraint) {
                    sizes[i] = (int) (remainingSize * ((PercentConstraint) constraint).floatValue());
                    availableSize -= sizes[i];
                }
            }
        }

        // finally share the remaining space between the other components    
        ArrayList remaining = new ArrayList();
        for (int i = 0, c = components.length; i < c; i++) {
            if (components[i].isVisible()) {
                Constraint constraint = (Constraint) m_ComponentToConstraint.get(components[i]);
                if (constraint == REMAINING_SPACE) {
                    remaining.add(new Integer(i));
                    sizes[i] = 0;
                }
            }
        }

        if (remaining.size() > 0) {
            int rest = availableSize / remaining.size();
            for (Iterator iter = remaining.iterator(); iter.hasNext();) {
                sizes[((Integer) iter.next()).intValue()] = rest;
            }
        }

        // all calculations are done, apply the sizes
        int currentOffset = (HORIZONTAL == orientation ? insets.left : insets.top);

        for (int i = 0, c = components.length; i < c; i++) {
            if (components[i].isVisible()) {
                if (HORIZONTAL == orientation) {
                    components[i].setBounds(currentOffset, insets.top, sizes[i], d.height);
                } else {
                    components[i].setBounds(insets.left, currentOffset, d.width, sizes[i]);
                }
                currentOffset += gap + sizes[i];
            }
        }
    }

}
